﻿using System;
using System.Linq;
using System.Collections.Generic;
using System.Web;
using System.Xml;
using System.Xml.Linq;
using System.CodeDom;

namespace System.Web.ScriptBuilder
{
	public class ScriptLinkerBuildProvider : System.Web.Compilation.BuildProvider
	{
		private static readonly CodeThisReferenceExpression thisRef = new CodeThisReferenceExpression();

		private string m_typeName;

		public override void GenerateCode(System.Web.Compilation.AssemblyBuilder assemblyBuilder)
		{
			var stream = base.OpenStream();
			stream.Position = 0;

			m_typeName = CreateValidIdentifier(this.VirtualPath);

			var type = new CodeTypeDeclaration(m_typeName);
			type.BaseTypes.Add(new CodeTypeReference(typeof(ScriptLinkerBase)));
			type.Attributes = MemberAttributes.Public | MemberAttributes.Final;
			var typeCtr = new CodeConstructor();
			typeCtr.Attributes = MemberAttributes.Public;
			var collectScripts = new CodeMemberMethod();
			collectScripts.Name = "CollectScripts";
			collectScripts.ReturnType = new CodeTypeReference(typeof(void));
			collectScripts.Attributes = MemberAttributes.Public | MemberAttributes.Override;

			var reader = XmlReader.Create(stream, new XmlReaderSettings
			{
				CloseInput = true,
				ConformanceLevel = ConformanceLevel.Document,
				IgnoreComments = true,
				IgnoreWhitespace = true,
				IgnoreProcessingInstructions = false
			});
			try
			{
				reader.Read();

				while (reader.NodeType == XmlNodeType.XmlDeclaration) reader.Read();

				var root = (XElement)XElement.ReadFrom(reader);
				if (root == null || !string.Equals(root.Name.LocalName, "linker", StringComparison.OrdinalIgnoreCase))
					throw new XmlException(Loc.GetString("Exception_ScriptBuilderUnknownElementInDeclaration", root.Name, "composer"));

				var settings = root.Element("settings");
				var caching = root.Element("caching");
				var sources = root.Element("sources");
				var compression = root.Element("compression");

				SetSettings(settings, typeCtr);
				SetCaching(caching, typeCtr);
				SetCompression(compression, typeCtr);

				if (sources != null)
				{
					foreach (XElement declaration in sources.Elements())
					{
						if (string.Equals(declaration.Name.LocalName, "code"))
						{
							var statement = ReadCodeDeclaration(declaration);
							collectScripts.Statements.Add(statement);
						}
						else if (string.Equals(declaration.Name.LocalName, "file"))
						{
							var statement = ReadFileDeclaration(declaration);
							collectScripts.Statements.Add(statement);
						}
						else if (string.Equals(declaration.Name.LocalName, "directory"))
						{
							var statement = ReadDirectoryDeclaration(declaration);
							collectScripts.Statements.Add(statement);
						}
						else if (string.Equals(declaration.Name.LocalName, "linker"))
						{
							var statement = ReadCLinkerDeclaration(declaration);
							collectScripts.Statements.Add(statement);
						}
						else if (string.Equals(declaration.Name.LocalName, "files"))
						{
							var statement = ReadFilesDeclaration(declaration);
							collectScripts.Statements.Add(statement);
						}
						else
						{
							throw new XmlException(Loc.GetString("Exception_ScriptBuilderUnknownElementInDeclaration", root.Name, "code|file|directory|handler|files"));
						}
					}
				}
			}
			finally
			{
				reader.Close();
				stream.Dispose();
			}

			var compile = new CodeCompileUnit();
			var ns = new CodeNamespace("System.Web.ScriptBuilder");

			type.Members.Add(collectScripts);
			type.Members.Add(typeCtr);
			ns.Types.Add(type);
			compile.Namespaces.Add(ns);

			assemblyBuilder.AddCodeCompileUnit(this, compile);
		}

		private void SetCompression(XElement compression, CodeConstructor typeCtr)
		{
			if (compression == null)
				return;

			var type = (string)compression.Attribute("type");
			if (string.IsNullOrEmpty(type))
				return;

			var compressor = new CodePropertyReferenceExpression(
				new CodeTypeReferenceExpression(typeof(ScriptCompressor)),
				type
			);

			typeCtr.Statements.Add(new CodeVariableDeclarationStatement(
					typeof(Dictionary<string, string>), "settings",
					new CodeObjectCreateExpression(typeof(Dictionary<string, string>))
				)
			);

			var settingsRef = new CodeVariableReferenceExpression("settings");

			foreach (var attr in compression.Attributes())
			{
				if (attr.Name.LocalName == "type")
					continue;

				typeCtr.Statements.Add(
					new CodeMethodInvokeExpression(
						settingsRef,
						"Add",
						new CodePrimitiveExpression((string)attr.Name.LocalName),
						new CodePrimitiveExpression((string)attr)
					)
				);
			}

			typeCtr.Statements.Add(
				new CodeAssignStatement(
					new CodePropertyReferenceExpression(
						thisRef,
						"Compressor"
					),
					compressor
				)
			);

			typeCtr.Statements.Add(
				new CodeAssignStatement(
					new CodePropertyReferenceExpression(
						thisRef,
						"CompressorSettings"
					),
					settingsRef
				)
			);
		}
		private void SetCaching(XElement caching, CodeConstructor typeCtr)
		{
			if (caching == null)
				return;

			if (caching.Attribute("enabled") != null)
			{
				bool enabled;
				bool.TryParse((string)caching.Attribute("enabled"), out enabled);
				typeCtr.Statements.Add(
					new CodeAssignStatement(
						new CodePropertyReferenceExpression(
							thisRef,
							"CachingEnabled"
						),
						new CodePrimitiveExpression(enabled)
					)
				);
			}

			if (caching.Attribute("duration") != null)
			{
				int duration;
				int.TryParse((string)caching.Attribute("duration"), out duration);
				typeCtr.Statements.Add(
					new CodeAssignStatement(
						new CodePropertyReferenceExpression(
							thisRef,
							"CacheDuration"
						),
						new CodePrimitiveExpression(duration)
					)
				);
			}

			if (caching.Attribute("location") != null)
			{
				System.Web.UI.OutputCacheLocation location = (System.Web.UI.OutputCacheLocation)
					Enum.Parse(typeof(System.Web.UI.OutputCacheLocation), (string)caching.Attribute("location"), true);

				typeCtr.Statements.Add(
					new CodeAssignStatement(
						new CodePropertyReferenceExpression(
							thisRef,
							"CacheLocation"
						),
						new CodeFieldReferenceExpression(
							new CodeTypeReferenceExpression(
								typeof(System.Web.UI.OutputCacheLocation)
							),
							location.ToString()
						)
					)
				);
			}

			if (caching.Attribute("noStore") != null)
			{
				bool nostore;
				bool.TryParse((string)caching.Attribute("noStore"), out nostore);
				typeCtr.Statements.Add(
					new CodeAssignStatement(
						new CodePropertyReferenceExpression(
							thisRef,
							"CacheNoStore"
						),
						new CodePrimitiveExpression(nostore)
					)
				);
			}

			if (caching.Attribute("varyByContentEncoding") != null)
			{
				string varyBy = (string)caching.Attribute("varyByContentEncoding");
				typeCtr.Statements.Add(
					new CodeAssignStatement(
						new CodePropertyReferenceExpression(
							thisRef,
							"CacheVaryByContentEncoding"
						),
						new CodePrimitiveExpression(varyBy)
					)
				);
			}

			if (caching.Attribute("varyByHeader") != null)
			{
				string varyBy = (string)caching.Attribute("varyByHeader");
				typeCtr.Statements.Add(
					new CodeAssignStatement(
						new CodePropertyReferenceExpression(
							thisRef,
							"CacheVaryByHeader"
						),
						new CodePrimitiveExpression(varyBy)
					)
				);
			}

			if (caching.Attribute("varyByParam") != null)
			{
				string varyBy = (string)caching.Attribute("varyByParam");
				typeCtr.Statements.Add(
					new CodeAssignStatement(
						new CodePropertyReferenceExpression(
							thisRef,
							"CacheVaryByParam"
						),
						new CodePrimitiveExpression(varyBy)
					)
				);
			}
		}
		private void SetSettings(XElement settings, CodeConstructor typeCtr)
		{
			if (settings == null)
				return;

			if (settings.Attribute("stringBufferSize") != null)
			{
				int bufferSize;
				int.TryParse((string)settings.Attribute("stringBufferSize"), out bufferSize);
				typeCtr.Statements.Add(
					new CodeAssignStatement(
						new CodePropertyReferenceExpression(
							thisRef,
							"StringBufferSize"
						),
						new CodePrimitiveExpression(bufferSize)
					)
				);
			}

			if (settings.Attribute("contentType") != null)
			{
				var contentType = (string)settings.Attribute("contentType");
				typeCtr.Statements.Add(
					new CodeAssignStatement(
						new CodePropertyReferenceExpression(
							thisRef,
							"ContentType"
						),
						new CodePrimitiveExpression(contentType)
					)
				);
			}

            if (settings.Attribute("compression") != null)
			{
                bool compression;
                bool.TryParse((string)settings.Attribute("compression"), out compression);
				typeCtr.Statements.Add(
					new CodeAssignStatement(
						new CodePropertyReferenceExpression(
							thisRef,
                            "CompressionEnabled"
						),
                        new CodePrimitiveExpression(compression)
					)
				);
			}

            
		}

		private string CreateValidIdentifier(string p)
		{
			var ind = new System.Text.StringBuilder(p);

			for (int i = 0; i < ind.Length; i++)
			{
				if (!Char.IsLetterOrDigit(ind[i]))
					ind[i] = '_';
			}

			if (Char.IsDigit(ind[0]))
				ind[0] = '_';

			return ind.ToString();
		}

		private CodeMethodInvokeExpression ReadCodeDeclaration(XElement declaration)
		{
			var codeMethodCall = new CodeMethodInvokeExpression(
				thisRef,
				"AddCode",
				new CodePrimitiveExpression(declaration.Value)
			);
			return codeMethodCall;
		}
		private CodeMethodInvokeExpression ReadFileDeclaration(XElement declaration)
		{
			var path = (CodeExpression)GetPrimitive(declaration, "path");
			var encoding = (CodeExpression)GetEncoding(declaration);

			var codeMethodCall = new CodeMethodInvokeExpression(
				thisRef,
				"AddFile",
				path,
				encoding
			);
			return codeMethodCall;
		}
		private CodeMethodInvokeExpression ReadDirectoryDeclaration(XElement declaration)
		{
			var path = (CodeExpression)GetPrimitive(declaration, "path");
			var encoding = (CodeExpression)GetEncoding(declaration);

			var codeMethodCall = new CodeMethodInvokeExpression(
				thisRef,
				"AddDirectory",
				path,
				encoding
			);
			return codeMethodCall;
		}
		private CodeMethodInvokeExpression ReadCLinkerDeclaration(XElement declaration)
		{
			var path = (CodeExpression)GetPrimitive(declaration, "path");

			var codeMethodCall = new CodeMethodInvokeExpression(
				thisRef,
				"AddLinker",
				path
			);
			return codeMethodCall;
		}
		private CodeMethodInvokeExpression ReadFilesDeclaration(XElement declaration)
		{

			var include = (CodeExpression)GetListOf(declaration, "include");
			var exclude = (CodeExpression)GetListOf(declaration, "exclude");
			var encoding = (CodeExpression)GetEncoding(declaration);
			var sorter = (CodeExpression)GetSorter(declaration);

			var codeMethodCall = new CodeMethodInvokeExpression(
				thisRef,
				"AddFiles",
				include,
				exclude,
				sorter,
				encoding
			);

			return codeMethodCall;
		}

		private CodeExpression GetListOf(XElement declaration, string elementName)
		{
			var list = new CodeArrayCreateExpression(
				typeof(string),
				new CodePrimitiveExpression(declaration.Elements(elementName).Count())
			);

			foreach (XElement el in declaration.Elements(elementName))
			{
				var value = GetPrimitive(el, "path");
				list.Initializers.Add(value);
			}

			return list;
		}
		private CodeExpression GetSorter(XElement declaration)
		{
			if (declaration.Attribute("comparerType") != null)
			{
				var comp = (string)declaration.Attribute("comparerType");

				//Activator.CreateInstance(Type.GetType(<comp>));
				return new CodeMethodInvokeExpression(
					new CodeTypeReferenceExpression(typeof(Activator)),
					"CreateInstance",
					new CodeMethodInvokeExpression(
						new CodeTypeReferenceExpression(typeof(Type)),
						"GetType",
						new CodePrimitiveExpression(comp)
					)
				);
			}
			return new CodePrimitiveExpression(null);
		}
		private CodeExpression GetPrimitive(XElement declaration, string attr)
		{
			if (declaration.Attribute(attr) == null)
				throw new XmlException(Loc.GetString("Exception_ScriptBuilderMissingRequiredAttribute", declaration, attr));

			return new CodePrimitiveExpression((string)declaration.Attribute(attr));
		}
		private CodeExpression GetEncoding(XElement declaration)
		{
			if (declaration.Attribute("encoding") != null)
			{
				var encStr = (string)declaration.Attribute("encoding");

				try
				{
					System.Text.Encoding.GetEncoding(encStr);
				}
				catch (ArgumentException)
				{
					throw new XmlException(Loc.GetString("Exception_ScriptBuilderWrongContentEncoding", declaration, encStr));
				}

				return new CodeMethodInvokeExpression(
					new CodeTypeReferenceExpression(typeof(System.Text.Encoding)),
					"GetEncoding",
					new CodePrimitiveExpression(encStr)
				);
			}
			return new CodePrimitiveExpression(null);
		}

		public override Type GetGeneratedType(System.CodeDom.Compiler.CompilerResults results)
		{
			return results.CompiledAssembly.GetType("System.Web.ScriptBuilder." + m_typeName);
		}
	}
}